/*
 * #%L
 * Server
 * %%
 * Copyright (C) 2011 Ractoc.com
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package com.ractoc.opengamefinder.server.plugins;

import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;

import org.apache.log4j.Logger;

import com.ractoc.opengamefinder.api.results.MessageResultsErrorCodes;
import com.ractoc.opengamefinder.server.SessionObject;
import com.ractoc.opengamefinder.server.plugins.messages.CloseSessionMessage;
import com.ractoc.opengamefinder.server.plugins.messages.ResetSessionTimeoutMessage;
import com.ractoc.opengamefinder.server.plugins.messages.RetrieveSessionValueMessage;
import com.ractoc.opengamefinder.server.plugins.messages.ServerMessage;
import com.ractoc.opengamefinder.server.plugins.messages.SessionTimeoutMessage;
import com.ractoc.opengamefinder.server.plugins.messages.StartSessionMessage;
import com.ractoc.opengamefinder.server.plugins.messages.StoreSessionValueMessage;
import com.ractoc.opengamefinder.server.plugins.results.CloseSessionResult;
import com.ractoc.opengamefinder.server.plugins.results.RetrieveSessionValueResult;
import com.ractoc.opengamefinder.server.plugins.results.StartSessionResult;
import com.ractoc.pffj.api.BasePlugin;
import com.ractoc.pffj.api.BasePluginMessageResult;
import com.ractoc.pffj.api.PluginLoaderException;
import com.ractoc.pffj.api.PluginMessage;
import com.ractoc.pffj.api.PluginMessageResultListener;
import com.ractoc.pffj.core.PluginController;
import com.ractoc.pffj.core.PluginException;

/**
 * Plugin which contains and handles all the session objects on the server.
 *
 * @author ractoc
 * @version 0.1
 */
public class SessionPlugin extends BasePlugin implements Callable<Integer> {

    private static final String SESSION_CLOSE_EVENT_LISTENER_SEPERATOR = ",";
    private static final String SENDING_TIMEOUT_MESSAGE = "sending timeout to ";
    private static final String UNABLE_SEND_TIMEOUT = "Unable send timeout messsage for sesion id ";
    private static Logger logger = Logger.getLogger(SessionPlugin.class.getName());
    private static String[] sessionCloseEventListeners;
    private static final ConcurrentHashMap<Integer, SessionObject> SESSION_OBJECTS =
            new ConcurrentHashMap<Integer, SessionObject>();
    private long sessionTimeout = 20L * 60L;

    /**
     * Default constructor.
     */
    public SessionPlugin() {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void load() throws PluginLoaderException {
        // here we start a seperate thread to handle session timeouts.
        Executors.newSingleThreadExecutor().submit(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void processMessage(final PluginMessage msg, final PluginMessageResultListener pmrListener) {
        BasePluginMessageResult result = null;
        if (msg instanceof ResetSessionTimeoutMessage) {
            result = processResetTimeout(msg);
        } else if (msg instanceof RetrieveSessionValueMessage) {
            result = processRetreiveValue(msg);
        } else if (msg instanceof StoreSessionValueMessage) {
            result = processStoreValue(msg);
        } else if (msg instanceof StartSessionMessage) {
            result = processStartSession(msg);
        } else if (msg instanceof CloseSessionMessage) {
            result = processCloseSession(msg);
        }
        pmrListener.receivedResult(result);
    }

    private BasePluginMessageResult processCloseSession(final PluginMessage msg) {
        BasePluginMessageResult result;
        final CloseSessionMessage csMsg = (CloseSessionMessage) msg;
        final SessionObject session = SESSION_OBJECTS.remove(csMsg.getSessionId());
        if (session != null) {
            // now we notify each of the registered plugins.
            Integer playerId = (Integer) session.getVariable(SessionObject.PLAYER_ID);
            // if no playerId was found, the user never actually logged in.
            if (playerId != null) {
                csMsg.setPlayerId(playerId);
                final ServerMessage sMsg = new ServerMessage();
                sMsg.setSessionId(csMsg.getSessionId());
                sMsg.setPayload(csMsg);
                for (String listener : sessionCloseEventListeners) {
                    try {
                        PluginController.getCurrentPluginController().processMessage(listener, sMsg);
                    } catch (final PluginException e) {
                        logger.error(UNABLE_SEND_TIMEOUT + sMsg.getSessionId(), e);
                    }
                }
            }
        }
        final CloseSessionResult sResult = new CloseSessionResult();
        sResult.setErrorCode(MessageResultsErrorCodes.SUCCESS);
        result = sResult;
        return result;
    }

    private BasePluginMessageResult processStartSession(final PluginMessage msg) {
        BasePluginMessageResult result;
        final StartSessionMessage sMsg = (StartSessionMessage) msg;
        logger.debug("creating session for " + sMsg.getSessionId());
        final SessionObject session = new SessionObject();
        session.setSessionId(sMsg.getSessionId());
        SESSION_OBJECTS.put(sMsg.getSessionId(), session);
        final StartSessionResult sResult = new StartSessionResult();
        sResult.setErrorCode(MessageResultsErrorCodes.SUCCESS);
        result = sResult;
        return result;
    }

    private BasePluginMessageResult processStoreValue(final PluginMessage msg) {
        BasePluginMessageResult result;
        final StoreSessionValueMessage sMsg = (StoreSessionValueMessage) msg;
        SESSION_OBJECTS.get(sMsg.getSessionId()).setVariable(sMsg.getKey(), sMsg.getValue());
        final RetrieveSessionValueResult sResult = new RetrieveSessionValueResult();
        sResult.setValue(SESSION_OBJECTS.get(sMsg.getSessionId()).getVariable(sMsg.getKey()));
        result = sResult;
        return result;
    }

    private BasePluginMessageResult processRetreiveValue(final PluginMessage msg) {
        BasePluginMessageResult result;
        final RetrieveSessionValueMessage sMsg = (RetrieveSessionValueMessage) msg;
        final RetrieveSessionValueResult sResult = new RetrieveSessionValueResult();
        sResult.setValue(SESSION_OBJECTS.get(sMsg.getSessionId()).getVariable(sMsg.getKey()));
        result = sResult;
        return result;
    }

    private BasePluginMessageResult processResetTimeout(final PluginMessage msg) {
        BasePluginMessageResult result;
        final ResetSessionTimeoutMessage rstMsg = (ResetSessionTimeoutMessage) msg;
        SESSION_OBJECTS.get(rstMsg.getSessionId()).resetSessionTimeout();
        final StartSessionResult sResult = new StartSessionResult();
        sResult.setErrorCode(MessageResultsErrorCodes.SUCCESS);
        result = sResult;
        return result;
    }

    /**
     * Return the supplied session timout.
     * 
     * @return The supplied session timeout.
     */
    public final Long getSessionTimeout() {
        return sessionTimeout;
    }

    /**
     * Set the supplied session timeout.
     * 
     * @param sessionTimeoutParam
     *            The supplied session timeout.
     */
    public final void setSessionTimeout(final Long sessionTimeoutParam) {
        this.sessionTimeout = sessionTimeoutParam;
    }

    /**
     * Return the supplied SessionCloseEventListeners. These are the plugins
     * that want to be notified of a closing session.
     * 
     * @return the supplied list of pluginIds in a comma seperated list.
     */
    public final String getSessionCloseEventListeners() {
        String retVal = "";
        for (String listener : sessionCloseEventListeners) {
            if (retVal.length() > 0) {
                retVal += SESSION_CLOSE_EVENT_LISTENER_SEPERATOR;
            }
            retVal += listener;
        }
        return retVal;
    }

    /**
     * Set the list of plugins that want to be notified of a closing session.
     * @param val The list of plugins in a comma seperated list.
     */
    public final void setSessionCloseEventListeners(final String val) {
        sessionCloseEventListeners = val.split(SESSION_CLOSE_EVENT_LISTENER_SEPERATOR);
        for (int i = 0; i < sessionCloseEventListeners.length; i++) {
            sessionCloseEventListeners[i] = sessionCloseEventListeners[i].trim();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Integer call() throws Exception {
        while (true) {
            try {
                // we sleep for half the timeout interval. This makes sure we
                // don't take too much processing power while still having a
                // very close approximation of the session timeout.
                //logger.debug("sleeping for " + (sessionTimeout * 1000L / 2L) + "ms");
                Thread.sleep(sessionTimeout * 1000L / 2);
            } catch (final InterruptedException e) {
                logger.debug("session timeout check sleep interrupted");
            }
            //logger.debug("waking up at " + System.currentTimeMillis());
            for (SessionObject session : SESSION_OBJECTS.values()) {
                logger.debug("timeout session " + session.getSessionId() + ": "
                        + (session.getLastActivityTime().getTime() + (sessionTimeout * 1000L)));
                if (System.currentTimeMillis() > session.getLastActivityTime().getTime() + (sessionTimeout * 1000L)) {
                    // the session has timed out, we need to notify the network
                    // plugin of this fact and remove the session.
                    final SessionTimeoutMessage sMsg = new SessionTimeoutMessage();
                    sMsg.setSessionId(session.getSessionId());
                    try {
                        logger.info(SENDING_TIMEOUT_MESSAGE + sMsg.getSessionId());
                        PluginController.getCurrentPluginController().processMessage("networkPlugin", sMsg);
                    } catch (final PluginException e) {
                        logger.error(UNABLE_SEND_TIMEOUT + session.getSessionId(), e);
                    }
                }
            }
        }
    }
}
